/*
 * Copyright 2010-2012 Susanta Tewari. <freecode4susant@users.sourceforge.net>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package coalescent.data;

import coalescent.model.K69;
import coalescent.phylogeny.CheckPPhylogenyAlgo;

import commons.util.Adders;
import commons.util.Adders.ByteAdder;
import commons.util.BinaryDataUtil;
import commons.util.ExceptionUtil;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.openide.util.NbBundle;

import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

/**
 * @param <A> allele type
 * @param <O> mutation type
 * @param <R> the runtime type of this class
 * @author Susanta Tewari
 */
public abstract class K69Data<A, O, R extends K69Data<A, O, R>> extends KC64Data<A, K69, R> {

    /**
     * @param model model
     * @param alleleFreq allele frequency
     * {@link CheckPPhylogenyAlgo phylogeny}.
     */
    public K69Data(K69 model, Map<A, Integer> alleleFreq) {
        super(model, alleleFreq);
    }

    /**
     * Method description
     *
     *
     * @return
     */
    @Override
    public abstract MutableK69Data<A, O, R> getMutable();

    /**
     * Immediate mutations are mutations carried by only one allele in the data set. Alternatively,
     * they are also called singleton mutations.
     *
     * @param mutationAllele an allele in this data set with immediate mutation(s); one of the
     * alleles obtained via {@code getAlleles(MUTATION)}
     * @return the immediate mutations of the specified allele
     * {@code getAlleles(MUTATION)}
     */
    public abstract Set<O> getImmediateMutations(A mutationAllele);

    /**
     * Checks if the specified allele carries an immediate mutation for which the mutated type is
     * present in the data set. Such a mutation is called merge mutation.
     *
     * @param allele an allele in this data set with immediate mutation
     * @param mutation an immediate mutation carried by the specified allele
     * @return {@code true} if the specified mutation is a merge mutation; {@code false} otherwise
     */
    public final boolean isMergeMutation(A allele, O mutation) {

        checkMutationAllele(allele, mutation);

        return getAlleles().contains(mutateBackward(allele, mutation));
    }

    /**
     * Checks if the specified mutation is an immediate mutation that is carried by the specified
     * allele.
     *
     * @param allele an allele in this data set with the specified immediate mutation
     * @param mutation an immediate mutation carried by the specified allele
     */
    protected final void checkMutationAllele(A allele, O mutation) {


        // NullPointerException for mutation
        if (mutation == null) {
            ExceptionUtil.throwIllegalArgExNull("mutation");
        }


        // NullPointerException for allele; IllegalArgumentException 1 & 2
        checkMutationAllele(allele);


        // IllegalArgumentException 3
        if (!getImmediateMutations(allele).contains(mutation)) {

            final String msg = NbBundle.getMessage(BinaryDataUtil.class,
                                   "Mutation_Not_Belons_Allele", mutation, allele);

            throw new IllegalArgumentException(msg);
        }
    }

    /**
     * Returns an allele without the specified mutation in the specified allele. The returned allele
     * might be a new instance or an exiting allele in this data set. Backward mutation means
     * losing the mutation whereas the forward would mean introducing it.
     *
     * @param allele an allele in this data set with the specified immediate mutation
     * @param mutation an immediate mutation carried by the specified allele
     * @return an allele without the specified mutation in the specified allele
     */
    public abstract A mutateBackward(A allele, O mutation);

    /**
     * Infinite-sites model (ISM) can be contrasted with infinite-alleles model (IAM) in that
     * mutations in an allele in ISM are visible and countable.
     *
     * @return total number of mutations in this data set
     */
    public abstract int getMutationCount();

    /**
     * A wild allele is an allele with no mutations. It may or may not exist in this data set. Even
     * if it exists the returned allele may not be the same reference.
     *
     * @return the wild allele which may or may not be present in this data set
     */
    public abstract A getWildAllele();

    /**
     * Integer mutation type is defined by the index (0-based) of the site on the allele.
     *
     * @param model model
     * @param data keys are binary strings
     * @return new instance of data
     */
    public static K69Data<String, Integer, ?> of(K69 model, Map<String, Integer> data) {
        return new StringK69Data(model, data);
    }

    /**
     * Class description
     *
     *
     * @version        Enter version here..., 12/10/09
     * @author         Susanta Tewari
     */
    public static class StringK69Data extends K69Data<String, Integer, StringK69Data> {

        /** Field description */
        private final int mutationCount;

        /** Field description */
        private final int[] siteMutationCount;

        /**
         * @param model model
         * @param alleleFreq allele frequency
         * <li> the alleles in {@code alleleFreq} do not form a phylogeny</li>
         * </ol>
         */
        public StringK69Data(K69 model, Map<String, Integer> alleleFreq) {

            super(model, alleleFreq);

            String startAllele = alleleFreq.keySet().iterator().next();
            int alleleLength   = startAllele.length();


            // key not binary string / keys don`t have the same length
            for (String allele : alleleFreq.keySet()) {


                // key not binary string
                if (!Pattern.matches("[01]", allele)) {

                    final String msg = NbBundle.getMessage(BinaryDataUtil.class,
                                           "K69Data_Allele_Binary_String_Invaild", allele);

                    throw new IllegalArgumentException(msg);
                }


                // keys don`t have the same length
                if (alleleLength != allele.length()) {

                    final String msg = NbBundle.getMessage(BinaryDataUtil.class,
                                           "K69Data_Alleles_Binary_String_Length_Mismatch", allele);

                    throw new IllegalArgumentException(msg);
                }
            }


            // a non-segregating site is present
            for (int i = 0; i < alleleLength; i++) {

                char site              = startAllele.charAt(i);
                boolean nonSegregating = true;

                for (String allele : alleleFreq.keySet()) {

                    if (allele.charAt(i) != site) {

                        nonSegregating = false;

                        break;
                    }
                }

                if (nonSegregating) {

                    final String msg = NbBundle.getMessage(BinaryDataUtil.class,
                                           "Non_Segregating_Site", (i + 1));

                    throw new IllegalArgumentException(msg);
                }
            }

            this.mutationCount = alleleLength;


            // site mutation count
            ByteAdder adder = Adders.createByteAdder(mutationCount);


            // checking phylogeny
            if (!CheckPPhylogenyAlgo.GUSFIELD.isPhylogeny2(alleleFreq.keySet())) {
                ExceptionUtil.throwIllegalArgEx(getClass(), "K69Data_Not_A_Phylogeny");
            }

            this.siteMutationCount = adder.sum();
        }

        /**
         * Method description
         *
         *
         * @param allele
         *
         * @return
         */
        @Override
        public Set<Integer> getImmediateMutations(String allele) {

            checkMutationAllele(allele);

            /**
             * Algorithm: If the character is '1' and the site mutation count is also 1,
             * then it`s in. Site mutation count > 1 means, the mutation is present in more
             * than one allele and thus is not an immediate mutation (a mutation that cannot be
             * lost in the next event)
             *
             */
            char[] char_array   = allele.toCharArray();
            Set<Integer> result = new HashSet<>(char_array.length);

            for (int i = 0; i < char_array.length; i++) {

                if ((siteMutationCount[i] == 1) && (char_array[i] == '1')) {
                    result.add(i);
                }
            }

            return result;
        }

        /**
         * Method description
         *
         *
         * @param allele
         * @param mutation
         *
         * @return
         */
        @Override
        public String mutateBackward(String allele, Integer mutation) {

            checkMutationAllele(allele, mutation);

            char[] char_array = allele.toCharArray();

            char_array[mutation] = '0';

            return new String(char_array);
        }

        /**
         * Method description
         *
         *
         * @return
         */
        @Override
        public int getMutationCount() {
            return mutationCount;
        }

        /**
         * Method description
         *
         *
         * @return
         */
        @Override
        public String getWildAllele() {
            return StringUtils.repeat('0', mutationCount);
        }

        /**
         * Method description
         *
         *
         * @return
         */
        @Override
        public MutableK69Data<String, Integer, StringK69Data> getMutable() {

            return new MutableK69Data<String, Integer, StringK69Data>(getModel(), getAlleleFreq()) {

                @Override
                public StringK69Data getImmutable() {
                    return new StringK69Data(getModel(), getAlleleFreq());
                }
                @Override
                public void removeMutation(Integer mutation) {

                    Map<String, Integer> alleleFreqMap = getAlleleFreq();

                    for (String allele : alleleFreqMap.keySet()) {

                        String newAllele = new String(ArrayUtils.remove(allele.toCharArray(),
                                               mutation));

                        setAllele(allele, newAllele);
                    }
                }
            };
        }

        /**
         * Method description
         *
         *
         * @param data
         *
         * @return
         */
        @Override
        public StringK69Data factory(StringK69Data data) {
            return new StringK69Data(data.getModel(), data.getAlleleFreq());
        }
    }
}
